为何须要扩充Lottie?原生Lottie的抖音抖音的赞不足袁记短视频热门业务教程网
Lottie相信端侧开发的朋友一定十分熟悉,打一出世就技惊四座,买赞直接将动漫开发的小时显示抖音人气业务供货商效率提升到了极高的级别,将我们开发从动漫的自助深渊中一把拽出,可以说没有Lottie之前碰到动漫的平台项目毛发掉一地,有了Lottie后的不出动漫需求真就保温杯里泡沙参了。
于是抖音抖音的赞我们可以渐渐欣赏Lottie呈现出以下疗效
随着业务迭代,设计师er将动漫又推向了一个新的买赞高度,早已不仅仅满足做一下展示型动漫了,小时显示她们想在更多的自助业务场景加入动漫来提升交互体验,例如拆个红包,平台砍个价等等
右图拆红包动漫供你们参考:
保温杯是不出否能够握得住了?
我们的需求
如上图所示是一个开红包的动漫,动漫中的抖音抖音的赞抢按键可点击,红包结果页的买赞让利券信息是插口动态下发,下边的小时显示金币,双击数,星星数都是用户独有的,不晓得你们工作中有没有类似场景呢?
抖音电商的抖音人气业务供货商场景下则有好多类似涉及动态业务数据的交互动漫,但这类需求我们就无法继续使用Lottie了,被迫又回归到最原始的原生代码方案,开发效率一下回到解放前,因而这类场景的开发效率亟需增强。
前期打算方案督查
需求场景明晰后接出来就是核高基方案了,我们先对功能做个拆解可以发觉我们动漫中须要满足动态替换文本,且文本的背景须要自适应拉伸,应当还有其他场景例如贴图的替换等,再加上按键的点击交互风波。
了解到我们的目标功能后则须要从Lottie开放或半开放的能力中找到切入点
Lottie给我们提供了替换文本和贴图的能力,这种能力是否能满足我们的需求呢?
Lottie可以替换文本和贴图,因而上述的动漫场景中文本可以动态替换
但做不到:
简单版方案
假如暂不考虑按键点击风波的话(有一些比较粗糙的方案来做点击)和动态控件疗效(并不是十分普遍的场景),我们是否有方案可以支持上述功能呢?
我们把思维打开一下,这种动态数据是否和原生的一个xml布局填充数据后十分相像?那既然Lottie支持动态替换贴图的话,我们是否可以动态生成贴图之后再进行替换呢?
其实是可以的,我们可以将动漫中所有动态的部份在动漫中用一张贴图占位,之后运行时动态将布局转换成贴图对占位贴图做一个替换,这样我们的动漫就实现了业务数据的动态绑定了
写了个简单的demo验证了该方案是可行的,如右图
第一步:将动态布局生成bitmap(相关代码网上好多)
/** * 获取已经显示的view的bitmap * @param view * @return */ public static Bitmap getCacheBitmapFromView(View view) { final boolean drawingCacheEnabled = true; view.setDrawingCacheEnabled(drawingCacheEnabled); view.buildDrawingCache(drawingCacheEnabled); final Bitmap drawingCache = view.getDrawingCache(); Bitmap bitmap = null; if (drawingCache != null) { bitmap = Bitmap.createBitmap(drawingCache); view.setDrawingCacheEnabled(false); } return bitmap; } /** * 获取未显示的view的bitmap * @param view * @param width * @param height * @return */ public static Bitmap getBitmapFromView(View view, int width, int height) { layoutView(view, width, height); return getCacheBitmapFromView(view); } /** * 布局控件 * @param view * @param width * @param height */ private static void layoutView(View view, int width, int height) { view.layout(0, 0, width, height); int measuredWidth = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY); int measuredHeight = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY); view.measure(measuredWidth, measuredHeight); view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); }
第二步:通过LottieAssetDelegate动态替换掉占位贴图即可
public class LottieAssetDelegate implements ImageAssetDelegate { private Context context; private String replaceImgName; private Bitmap replaceBitmap; private String imagesFolder; public LottieAssetDelegate(Context context, String replaceImgName, Bitmap replaceBitmap, String imagesFolder) { this.context = context; this.replaceImgName = replaceImgName; this.replaceBitmap = replaceBitmap; if (!TextUtils.isEmpty(imagesFolder) && imagesFolder.charAt(imagesFolder.length() - 1) != '/') { this.imagesFolder = imagesFolder + '/'; } else { this.imagesFolder = imagesFolder; } } @Nullable @Override public Bitmap fetchBitmap(LottieImageAsset asset) { if (replaceImgName.equals(asset.getFileName())) { return replaceBitmap; } return getBitmap(asset); } private Bitmap getBitmap(LottieImageAsset asset) { Bitmap bitmap = null; String filename = asset.getFileName(); BitmapFactory.Options opts = new BitmapFactory.Options(); opts.inScaled = true; opts.inDensity = 160; InputStream is; try { is = context.getAssets().open(imagesFolder + filename); } catch (IOException e) { return null; } try { bitmap = BitmapFactory.decodeStream(is, null, opts); } catch (IllegalArgumentException e) { return null; } return bitmap; }}
进阶版方案
简单版方案可以满足一些需求,而且不够完美,好多场景受限,假如我须要替换的部份是一个倒计时呢?如上图上面的让利券正式过期的那个文本是个倒计时,设计师须要倒计时运行上去的抖音的赞显示不出来的,但简单版的方案由于是生成静态贴图难以做到更新,所以简单版本的方案是还不错,但总感觉没有血肉,不够强壮有力!
成年人的世界为何不能全都要?我们要支持未来可能遇见的所有场景,我们要完美的支持点击,我们要完美的支持动态业务数据,我们也要完美的支持动态组件,我们要Lottie能像我们希望的那样支持我们的功能。
那就让我们把思路彻底打开,是否可以将占位贴图替换成原生的布局控件呢?也即是在渲染占位贴图的时侯直接换成渲染原生布局,这样动漫和原生布局就无缝衔接在一起
原理示例图
我们的选择场景覆盖业务逻辑动态布局点击交互扩充性
简单版
60%
支持
不支持
不支持
差
进阶版
100%
支持
支持
支持
好
和简单版方案做个对比就可以很容易作出选择
方案介绍核心原理
方案的核心原理是创建一个动态布局视口DynamicLayoutLayer,和Lottie上面支持的ImageLayer、TextLayer、CompostionLayer一样,由自己来实现勾画逻辑,之后在运行期间hook原动漫占位视口(ImageLayer),替换成DynamicLayoutLayer,占位视口上所有属性变换都代理到DynamicLayoutLayer上,进而实现无缝替换。
类图如下:
核心问题
要实现该方案须要解决其中几个核心的问题,首先要解决视口的同层渲染问题让替换的视口和原始占位视口在同一个层级进行渲染,就能实现无缝衔接,其次原始视口的动漫疗效也须要同步给替换的视口,这样作用在原始视口上的动漫变换疗效能够在替换视口上彰显,最后须要解决下点击交互风波和布局动态刷新的问题,能够完整的支持所有需求场景,下边会对每位核心问题做详尽方案剖析。
下文贴的代码均非即将代码,只做大致原理理解
Lottie的每位视口还会调用自身的draw来勾画到canvas上,假如要做到替换后实现同层渲染则也须要将native控件根据占位图层层级勾画到Lottie的canvas上,因而我们的解决方案就是将占位视口的勾画代理到DynamicLayoutLayer,将Lottie的画布传入,之后调用DynamicLayoutLayer的勾画逻辑将内容勾画到传入的画布中即可
示例代码:
static BaseLayer forModel( Layer layerModel, LottieDrawable drawable, LottieComposition composition) { switch (layerModel.getLayerType()) { case SHAPE: return new ShapeLayer(drawable, layerModel); case PRE_COMP: return new CompositionLayer(drawable, layerModel, composition.getPrecomps(layerModel.getRefId()), composition); case SOLID: return new SolidLayer(drawable, layerModel); case IMAGE: //判断是否是动态布局图层 是则替换成DynamicLayoutLayer if (isDynamicLayout(layerModel)) { return new DynamicLayoutLayer(drawable, layerModel); } return new ImageLayer(drawable, layerModel); case NULL: return new NullLayer(drawable, layerModel); case TEXT: return new TextLayer(drawable, layerModel); case UNKNOWN: default: // Do nothing L.warn("Unknown layer type " + layerModel.getLayerType()); return null; } }
public class DynamicLayoutLayer extends BaseLayer{ ...... @Override void drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) { //动态布局绘制 }}
替换后视口的层级问题解决了,而且视口上绑定的动漫也须要同步到替换视口上,这是我们须要解决的第二个困局,动漫的问题我们须要从Lottie动漫的原理来入手,须要了解两个概念帧时间轴和Matrix变换
帧时间轴
Lottie动漫数据是由无数个关键帧组成的,设计师在每一个关键帧上设置属性数据,则两个关键帧之间就是数据的变换,我把这个称做帧时间轴,Lottie动漫的原理就是随着帧轴运行时估算出当前帧的属性数据,再把数据设置给视口,通过每位视口在对应帧同步对应的属性数据进而达到动漫的疗效。
举个简单的事例,我在第1帧设置了一个缩放的关键帧,数据设置成100%,之后在第5帧上设置一个缩放关键帧,数据设置成50%,再在第10帧设置缩放关键帧,数据150%,则呈现下来的动漫疗效就是该视口从开始原始大小在5帧的时间内缩小到50%,再5帧的时间内从50%放大到150%,之后再动漫运行的时侯随着动漫浏览到的帧率估算当前帧的数据,例如第一帧的时侯数据为100%,之后浏览到第2帧的时侯估算出数据为90%,把数据设置给视口,以这种推每一帧都估算出自己的数据进行设置,串上去就产生的动漫疗效
Matrix变换
Matrix是一种矩阵变换,通常图象处理上会使用到,在Android中也有大量应用场景,我们熟知的View的一些属性变换疗效都是Matrix来实现的,通过Matrix的变换可以改变View的属性,例如缩放值、位移值、旋转角度等,而Lottie的动漫疗效也是使用Matrix数据变换来得到的,AE上面导入的数据会转换成一组Matix,在每帧渲染的时侯估算出对应的Matrix数据之后设置给layer,进而实现了视口的属性变换疗效,而视口就是组成Lottie动漫的基础元素,所有视口结合上去就是完整的Lottie动漫了
关于Matrix的相关知识点可自行学习抖音的赞显示不出来的,这儿只引入概念
通过对动漫原理的剖析我们要解决动漫同步的问题就很简单了,只须要将原先动漫中应用到占位视口上的基础数据和matrix变换数据全部代理给动态布局视口即可
示例代码:
//动态布局图层绘制void drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) { View view = getReplaceView(); //重点是下面这段代码,将matrix设置给画布,再将原生控件绘制到当前画布上 canvas.save(); canvas.concat(parentMatrix); view.draw(canvas);}
解决了以上两个问题,我们的方案大致完成了60%,但Lottie动漫的一个最大的痛点问题就是点击风波,大部份的Lottie动漫虽然没有动态的业务数据并且按键点击的需求是大机率会有的,而在之前我使用Lottie的时侯遇见点击的需求则直接在Lottie动漫之上对应位置添加一个虚拟的点击区域,是不是很粗糙暴力?那假如使用我们如今这个方案那点击风波是不是就不是问题了?
虽然还是有一点点小小的问题,由于我们的动态控件是替换占位视口的,动漫中会存在一些matrix的变换,变换后的控件位置就不是初始位置,也就是说你的matrix变换可能有位移或则缩放,致使点击区域错位,那这个问题如何解决呢?
虽然我们可以参考属性动漫,为何属性动漫缩放或则平移后点击区域也跟随调整了呢?虽然属性动漫的内部有做一个matrix的反向矫治,我们同样可以参考这块的实现对区域做一个矫治处理即可
示例代码:
private MotionEvent getTransformedMotionEvent(MotionEvent event, View child) { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; final MotionEvent transformedEvent = MotionEvent.obtain(event); transformedEvent.offsetLocation(offsetX, offsetY); if (!child.hasIdentityMatrix()) { transformedEvent.transform(child.getInverseMatrix()); } return transformedEvent;}public final Matrix getInverseMatrix() { ensureTransformationInfo(); if (mTransformationInfo.mInverseMatrix == null) { mTransformationInfo.mInverseMatrix = new Matrix(); } final Matrix matrix = mTransformationInfo.mInverseMatrix; mRenderNode.getInverseMatrix(matrix); return matrix;}
支持以上3个功能就早已满足我们大部份日常使用的场景了,虽然Lottie设计之初就是给我提供一个动漫展示的框架,并不能支持各类订制和功能扩充,且他的生命周期则很明晰动漫执行到结束(非循环动漫),倘若动漫有130帧,那Lottie就是从第一帧开始渲染,到130帧渲染结束,但假如有超出这个生命周期的动态布局还须要有更新则如何处理呢?例如我们里面红包开下来让利券的说明上面的有效期不是静态的文本而是一个倒计时,那在Lottie浏览到最后一帧后这个倒计时控件就没有办法继续走下去了,由于驱动倒计时重画的是Lottie的画布,Lottie由于生命周期早已结束,画布不在继续刷新,所对应的驱动力就割断了,因而这些场景下我们应当如何去解决呢?
只需提供一个重画刷新插口给到控件自己去触发即可
示例代码:
/** * 请求重绘 */public void redraw() { LottieAnimationView lottieAnimationView = getLottieAnimationView(); lottieAnimationView.invalidate();}
最终疗效
最终疗效入右图(左原图&慢放)
方案的利润
我们的Lottie扩充方案对我们来说有两个特别大的利润
第一利润就是提效,假如没有这套方案,我们就得回归到使用最原生的代码来实现动漫了,效率之低经历过的同学都有感受,至于扩充方案具体提效多少则和动漫的复杂度成反比,越复杂疗效越好!
第二个利润就是对Lottie源码的“掌控”能力,这儿用了“掌控”一词其实有些托大,但确实只有把Lottie的实现原理全理解了能够对Lottie进行大刀阔斧的扩充,理解原理后我们对Lottie的一些问题都可以自行更改且还可以扩充更多的特点,例如让Lottie支持音频?甚至支持视频资源等一些更中级的能力!
后续计划
目前方案还有一些不太常见的场景不支持,例如动漫里嵌入一个滚动的列表,再例如动漫的分段浏览逻辑(适宜做互动小游戏),在后续开发中若果有碰到类似需求则会考虑把相关场景扩充支持下,我们也会同步把方案思路分享给你们,同时该方案也会相继在我们内部其他项目组中试用,后期迭代稳定成熟后也会有开源的计划。
hi,我是抖音电商的HD
抖音电商无线技术团队正在招贤纳士!我们是公司的核心业务线,这儿云集了各路前辈,也饱含了机会与挑战.伴随着业务的高速发展,团队也在快速扩张.欢迎诸位前辈加入我们,一起创造世界级的电商产品~
热招岗位:Android/iOS中级开发,Android/iOS专家,Java构架师,产品总监(电商背景),测试开发...大量HC等你来呦~
内部推荐请发简历至>>>我们的邮箱:hr.ec@kuaishou.com
袁记短视频热门业务教程网